Micron Document
pintobyte rngit


Displaying Raw • Download

xous-core/loader/src/main.rs main (17e4bce8) Text, 46.14 KB

#![cfg_attr(not(test), no_main)]
#![cfg_attr(not(test), no_std)]

// Vex-II porting notes:
//
// Vex-II is currently supported as a "testing" stub. Compatibility to Vex-I is maintained
// through the "legacy-int" shim, which implements Vex-I style litex interrupt handling and
// masking through a CSR.
//
// ** Interrupts **
// The final version of Vex-II will introduce a PLIC. This will sit in between the litex
// interrupt controller and be largely a pass-through in Xous, but is provided so that other
// OSes which assume PLIC can port more easily to the SoC. This means that the stubs in
// kernel/src/arch/riscv/irq.rs (sim_read(), sim_write, sip_read()) will need to be re-written
// to either setup the PLIC and forget it or work the PLIC directly into the handler loop.
// Tasks:
// -[ ] Add PLIC into Vex-ii implementation
// -[ ] Refactor kernel/src/arch/riscv/irq.rs to handle this
//
// ** MMU **
// Vex-II includes support for -A and -D flags in the MMU. The current work-around in Xous is
// to simply set A for all R pages, and D for all W pages. Instead, A/D should be cleared
// in the loader, and inside the OS when a page is "read" but "A" is missing, A should be
// set and the read is retried. Likewise, when a page is "written" but "D" is missing,
// "D" should be set and the write retried. This implementation will allow the MMU to skip
// writing pages out to swap that are clean, and to also pick only pages that have not been
// accessed to consider as candidates for swapping out.
// Tasks:
// -[ ] Remove A/D "on by default" in loader/src/phase2.rs/ProgramDescription/Load() - multiple locations
// -[ ] Remove A/D "on by default" in kernel/src/arch/riscv/mem.rs/translate_flags()
// -[ ] Add handler for A/D trap inside
// kernel/src/arch/riscv/irq.rs/trap_handler/RiscvException::StoragePageFault | LoadPage Fault
// -[ ] Update swap handler to use the A/D features - not sure exact files, need to research this
//
// ** No curve25519 engine **
// To make space for the Vex-II core the curve25519 engine was gutted. This means there is no signature
// verification in the boot process. For the final Denarius implementation, this won't be a big impact
// because there is crypto hardware there on the SoC, but for Precursor validation model this means a lot
// of the L1 bootloader is stubbed out, and some stub code is put into the loader to "fake" the items
// now missing due to this omission. These are clearly delineated with a "vexii-test" fetaure flag.
//
// ** Clocking differences & TRNG **
// Vex-II on Precursor runs at 50MHz. This means that all of the other dependent clocks also have
// to down-clock. This breaks XADC and causes the TrngManager to fail test. Since this is a temporary
// config, the SoC bypasses the self-test and just causes the system to run in a more or less deterministic
// mode of operation. The TRNG will come from a different source on Denarius.
//
// ** Cache flush **
// Vex-II implements cmo.flush instructions. This means that the flush routine inside SPINOR needs
// to be handled differently: flushes need to address a specific address to flush.
// -[ ] services/spinor/src/lib.rs/Spinor/patch() has multiple locations where the API is modified
// to allow a specific virtual address to be pushed back to the flush routine.
//
// ** AES **
// Vex-II implements compliant AES instructions to the -Zkn standards (unlike Vex-I).
// The AES library has flags that recognize the "vexii-testing" feature, and implements acceleration
// for AES-256 (but falls back to software for AES-128). This will need to be revisited regardless
// for the bao1x/bao1x bring-up.
//
// ** Testing features **
// -[ ] libs/precursor/hal/src/board/precursors.rs - PDDB_LEN is shortened for vexii-test target

extern crate alloc;

#[macro_use]
mod args;
use args::{KernelArgument, KernelArguments};
#[cfg(feature = "bao1x")]
use bao1x_api::UUID;

#[cfg_attr(feature = "atsama5d27", path = "platform/atsama5d27/debug.rs")]
mod debug;
mod fonts;
#[cfg(all(feature = "secboot", not(feature = "vexii-test")))]
mod secboot;

#[cfg_attr(feature = "atsama5d27", path = "platform/atsama5d27/asm.rs")]
mod asm;
mod bootconfig;
#[cfg_attr(feature = "atsama5d27", path = "platform/atsama5d27/consts.rs")]
mod consts;
mod env;
mod minielf;
#[cfg(feature = "resume")]
mod murmur3;
mod phase1;
mod phase2;
mod platform;
#[cfg(feature = "swap")]
pub mod swap;

#[cfg(feature = "bao1x")]
use core::convert::TryInto;
use core::{mem, ptr, slice};

use asm::*;
#[cfg(feature = "bao1x")]
use bao1x_hal::board::{BOOKEND_END, BOOKEND_START};
use bootconfig::BootConfig;
use consts::*;
pub use loader::*;
use minielf::*;
use phase1::{InitialProcess, phase_1};
use phase2::{ProgramDescription, phase_2};
#[cfg(feature = "swap")]
use platform::SwapHal;

const WORD_SIZE: usize = mem::size_of::<usize>();
pub const SIGBLOCK_SIZE: usize = 0x1000;
const STACK_PAGE_COUNT: usize = 8;

const VDBG: bool = false; // verbose debug
const VVDBG: bool = false; // very verbose debug
const SDBG: bool = false; // swap debug

#[cfg(test)]
mod test;

// Bao1x needs a heap because the signature checking code assumes its availability.
// However the heap promises not to allocate any objects that are needed by the kernel,
// so we stick it in a region in low RAM, just about the statics, and restrict it to
// just 4k in size. Note that maybe we should strive to avoid the loader from allocating
// into these regions - but actually, later in the loader, we should avoid using the heaps
// and most data is done with, and it's nice to be able to reclaim these pages, so we
// somewhat dangerously tell the loader to go ahead and allocate over this region by
// telling it has the whole rest of SRAM to stick initial process pages in.
#[global_allocator]
static ALLOCATOR: linked_list_allocator::LockedHeap = linked_list_allocator::LockedHeap::empty();
// heap start is selected by looking at the total reserved .data + .bss region in the compiled loader.
// it hovers at 0x5000 unless I start adding lots of statics to the loader (which there are not).
pub const HEAP_OFFSET: usize = 0x5000;
// just a small heap, big enough for us to use alloc to simplify argument processing
pub const HEAP_LEN: usize = 0x1000;

#[repr(C)]
pub struct MemoryRegionExtra {
start: u32,
length: u32,
name: u32,
padding: u32,
}

/// A single RISC-V page table entry. In order to resolve an address,
/// we need two entries: the top level, followed by the lower level.
#[repr(C)]
pub struct PageTable {
entries: [usize; PAGE_SIZE / WORD_SIZE],
}

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum IniType {
// RAM
IniE,
// XIP
IniF,
// Swap
IniS,
}

/// Entrypoint
/// This makes the program self-sufficient by setting up memory page assignment
/// and copying the arguments to RAM.
/// Assume the bootloader has already set up the stack to point to the end of RAM.
///
/// # Safety
///
/// This function is safe to call exactly once.
#[export_name = "rust_entry"]
pub unsafe extern "C" fn rust_entry(signed_buffer: *const usize, signature: u32) -> ! {
let perclk_freq = crate::platform::early_init(); // sets up PLLs so we're not running at 16MHz...
// need to make this "official" for bao1x, the feature flag combo below works around some simulation
// config conflicts.
#[cfg(all(feature = "verilator-only", not(feature = "bao1x-mpw")))]
platform::coreuser_config();

// initially validate the whole image on disk (including kernel args)
// kernel args must be validated because tampering with them can change critical assumptions about
// how data is loaded into memory
#[cfg(all(feature = "secboot", not(feature = "vexii-test")))]
let mut fs_prehash = [0u8; 64];
#[cfg(not(all(feature = "secboot", not(feature = "vexii-test"))))]
let fs_prehash = [0u8; 64];
#[cfg(all(feature = "secboot", not(feature = "vexii-test")))]
if !secboot::validate_xous_img(signed_buffer as *const u32, &mut fs_prehash) {
loop {}
};

#[cfg(all(feature = "vexii-test", not(feature = "verilator-only")))]
{
// fake the prehash
use ed25519_dalek_loader::Digest;
#[repr(C)]
struct SignatureInFlash {
pub version: u32,
pub signed_len: u32,
pub signature: [u8; 64],
}
let sig_ptr = signed_buffer as *const SignatureInFlash;
let sig: &SignatureInFlash = unsafe { sig_ptr.as_ref().unwrap() };
let signed_len = sig.signed_len;
let image: &[u8] = unsafe {
core::slice::from_raw_parts(
(signed_buffer as usize + SIGBLOCK_SIZE) as *const u8,
signed_len as usize,
)
};
let mut h: sha2_loader::Sha512 = sha2_loader::Sha512::new();
h.update(&image);
let prehash = h.finalize();
fs_prehash.copy_from_slice(prehash.as_slice());

// ensure the crypto unit is on - necessary for TRNG to operate
let mut power = utralib::CSR::new(utralib::utra::power::HW_POWER_BASE as *mut u32);
power.rmwf(utralib::utra::power::POWER_CRYPTO_ON, 1);
let mut trng = utralib::CSR::new(utralib::utra::trng_server::HW_TRNG_SERVER_BASE as *mut u32);
trng.wo(utralib::utra::trng_server::CONTROL, 0x5); // disable avalanche generator

// debug output on TRNG unit.
println!(
"trng: cha/{:x} dat/{:x} sta/{:x}",
trng.r(utralib::utra::trng_server::CHACHA),
trng.r(utralib::utra::trng_server::DATA),
trng.r(utralib::utra::trng_server::STATUS)
);
let trngk = utralib::utra::trng_kernel::HW_TRNG_KERNEL_BASE as *const u32;
for i in 0..utralib::utra::trng_kernel::TRNG_KERNEL_NUMREGS {
println!("trngk{}: {:x}", i, trngk.add(i).read_volatile())
}
let trngs = utralib::utra::trng_server::HW_TRNG_SERVER_BASE as *const u32;
for i in 0..utralib::utra::trng_server::TRNG_SERVER_NUMREGS {
println!("trngs{}: {:x}", i, trngs.add(i).read_volatile())
}

// allow cache flushes to be initiated from supervisor and userspace
// I guess this creates a potential sidechannel, but we're not a multitenant
// threat model - if someone is running code on the SoC that can trigger
// cache flushes to exfil data, we've got bigger problems...?
core::arch::asm!(
"li t0, 0x30 | 0x40", // XENVCFG_CBIE_OK | XENVCFG_CBCFE_OK
"csrw 0x30a, t0", // allow supervisor
"csrw 0x10a, t0", // allow user
out("t0") _, // clobber t0
);
}

// Initialize the allocator with heap memory range. The heap memory is "throw-away"
// so we stick it near the bottom of RAM, with the assumption that the loader process
// won't smash over it.
#[cfg(feature = "bao1x")]
{
// heap is needed for bao1x-boot because signature depends on it
let heap_start = utralib::HW_SRAM_MEM + HEAP_OFFSET;
println!("Setting up heap @ {:x}-{:x}", heap_start, heap_start + HEAP_LEN);
unsafe {
ALLOCATOR.lock().init(heap_start as *mut u8, HEAP_LEN);
}
}

#[cfg(feature = "bao1x")]
let mut csprng = bao1x_hal::hardening::Csprng::new();
#[cfg(feature = "bao1x")]
csprng.random_delay();
#[cfg(feature = "bao1x")]
// it's security-important to ensure we're running off the PLL
bao1x_hal::hardening::check_pll();

// Run kernel image validation now that the heap is set up.
#[cfg(feature = "bao1x")]
let detached_app = {
use bao1x_api::{PARANOID_MODE, PARANOID_MODE_DUPE, bollard, pubkeys::LOADER_TO_KERNEL};
use bao1x_hal::{buram::ERASURE_PROOF_RANGE_BYTES, sigcheck::ERASE_VALUE};

let owc = bao1x_hal::acram::OneWayCounter::new();
let backup = bao1x_hal::buram::BackupManager::new();
let (paranoid1, paranoid2) = owc.hardened_get2(PARANOID_MODE, PARANOID_MODE_DUPE).unwrap();
let (dev_mode1, dev_mode2) = owc.hardened_get(bao1x_api::DEVELOPER_MODE).unwrap();
let (init1, init2) = owc.hardened_get(bao1x_api::IN_SYSTEM_BOOT_SETUP_DONE).unwrap();
csprng.random_delay();
bollard!(bao1x_hal::sigcheck::die_no_std, 4);
// validate using the bao1x signature scheme
match bao1x_hal::sigcheck::validate_image(LOADER_TO_KERNEL, None, Some(&mut csprng)) {
Ok((key, key_inv, tag, _target)) => {
if paranoid1 == 0 && paranoid2 == 0 {
// only print if not in paranoid mode; the DUART output can be used to align a glitch
println!(
"*** Kernel signature check by key @ {}/{}({}) OK ***",
key,
!key_inv,
core::str::from_utf8(&tag).unwrap_or("invalid tag")
);
}
if key != !key_inv {
bao1x_hal::sigcheck::die_no_std();
}
// we can't erase keys in the loader, because the keys have already been locked
// out at this point. Thus, ensure that the system is already in developer mode.
csprng.random_delay();
// k is just a nominal slot number. If either match, assume we are dealing with a
// developer image.
csprng.random_delay();
bollard!(bao1x_hal::sigcheck::die_no_std, 4);
// this has to gate on keys being initialized, because without key setup nothing gets erased
if tag == *bao1x_api::pubkeys::KEYSLOT_INITIAL_TAGS[bao1x_api::pubkeys::DEVELOPER_KEY_SLOT]
|| key == bao1x_api::pubkeys::DEVELOPER_KEY_SLOT
{
csprng.random_delay();
let erase_proof: &[u8; 32] =
backup.get_slice(ERASURE_PROOF_RANGE_BYTES).try_into().unwrap();
if dev_mode1 == 0 || (erase_proof != &[ERASE_VALUE; 32]) && (init1 != 0) {
if paranoid1 == 0 && paranoid2 == 0 {
println!("{}LOADER.KERNDIE,{}", BOOKEND_START, BOOKEND_END);
println!("Kernel is devkey signed, but system is not in developer mode. Dying!");
}
bao1x_hal::sigcheck::die_no_std();
} else {
// don't print anything on the first check, so a double-glitch is harder
}
}
csprng.random_delay();
// this consumes dev_mode2 & !k2 - the alternate version - preventing the check from being
// optimized out
bollard!(bao1x_hal::sigcheck::die_no_std, 4);
if !key_inv == bao1x_api::pubkeys::DEVELOPER_KEY_SLOT {
csprng.random_delay();
let erase_proof: &[u8; 32] =
backup.get_slice(ERASURE_PROOF_RANGE_BYTES).try_into().unwrap();
if dev_mode2 == 0 || (erase_proof != &[ERASE_VALUE; 32] && (init2 != 0)) {
if paranoid1 == 0 && paranoid2 == 0 {
println!("{}LOADER.KERNDIE,{}", BOOKEND_START, BOOKEND_END);
println!("Kernel is devkey signed, but system is not in developer mode. Dying!");
}
bao1x_hal::sigcheck::die_no_std();
} else {
println!("{}LOADER.KERNDEV,{}", BOOKEND_START, BOOKEND_END);
println!("Developer key detected on kernel. Proceeding in developer mode!");
}
}
bollard!(bao1x_hal::sigcheck::die_no_std, 4);
// Why no further hardening / duplicate sig check:
// - At this point, we're locked out of erasing keys anyways. If the adversary can get us to
// run dev keys signed code at this point, we have limited options to do reactive
// security.
// - The "prize" thus for the adversary at this point is to glitch the kernel or keystore
// directly. Need to think through the ramifications of e.g. ASID setting and how to
// harden that. There is no TOCTOU on the sig check versus code run because the keystore
// is always implemented in XIP code.
// - A duplicate sig check would be pretty expensive because the kernel is not small
}
Err(e) => {
println!("Kernel failed signature check. Dying: {:?}", e);
println!("{}LOADER.KERNFAIL,{}", BOOKEND_START, BOOKEND_END);
bao1x_hal::sigcheck::die_no_std();
}
}

if owc.get_decoded::<bao1x_api::BoardTypeCoding>().expect("Board type coding error")
== bao1x_api::BoardTypeCoding::Dabao
{
use bao1x_api::pubkeys::LOADER_TO_DETACHED_APP;

csprng.random_delay();
match bao1x_hal::sigcheck::validate_image(LOADER_TO_DETACHED_APP, None, Some(&mut csprng)) {
Ok((key, key_inv, tag, _target)) => {
if paranoid1 == 0 && paranoid2 == 0 {
println!(
"*** Detached app signature check by key @ {}/{}({}) OK ***",
key,
!key_inv,
core::str::from_utf8(&tag).unwrap_or("invalid tag")
);
}
// k is just a nominal slot number. If either match, assume we are dealing with a
// developer image.
if key != !key_inv {
bao1x_hal::sigcheck::die_no_std();
}
csprng.random_delay();
bollard!(bao1x_hal::sigcheck::die_no_std, 4);
if (tag
== *bao1x_api::pubkeys::KEYSLOT_INITIAL_TAGS[bao1x_api::pubkeys::DEVELOPER_KEY_SLOT]
|| key == bao1x_api::pubkeys::DEVELOPER_KEY_SLOT)
&& (init1 != 0)
{
let erase_proof: &[u8; 32] =
backup.get_slice(ERASURE_PROOF_RANGE_BYTES).try_into().unwrap();
// we can't erase keys in the loader, because the keys have already been locked
// out at this point. Thus, ensure that the system is already in developer mode.
if dev_mode1 == 0 || erase_proof != &[ERASE_VALUE; 32] {
if paranoid1 == 0 && paranoid2 == 0 {
println!("{}LOADER.APPDIE,{}", BOOKEND_START, BOOKEND_END);
println!(
"Detached app is devkey signed, but system is not in developer mode. Dying!"
);
}
bao1x_hal::sigcheck::die_no_std();
} else {
println!("{}LOADER.APPDEV,{}", BOOKEND_START, BOOKEND_END);
println!("Developer key detected on detached app. Proceeding in developer mode!");
}
}
csprng.random_delay();
bollard!(bao1x_hal::sigcheck::die_no_std, 4);
if (!key_inv == bao1x_api::pubkeys::DEVELOPER_KEY_SLOT) && (init2 != 0) {
let erase_proof: &[u8; 32] =
backup.get_slice(ERASURE_PROOF_RANGE_BYTES).try_into().unwrap();
if dev_mode2 == 0 || erase_proof != &[ERASE_VALUE; 32] {
if paranoid1 == 0 && paranoid2 == 0 {
println!("{}LOADER.APPDIE,{}", BOOKEND_START, BOOKEND_END);
println!(
"Detached app is devkey signed, but system is not in developer mode. Dying!"
);
}
bao1x_hal::sigcheck::die_no_std();
} else {
println!("{}LOADER.APPDEV,{}", BOOKEND_START, BOOKEND_END);
println!("Developer key detected on detached app. Proceeding in developer mode!");
}
}

// Why no further hardening - in theory, it's pretty "safe" to run developer
// images at this point, because kernel isolation should be protecting the keys.
true
}
Err(_e) => {
println!("{}LOADER.APPFAIL,{}", BOOKEND_START, BOOKEND_END);
println!("No valid detached app found");
false
}
}
} else {
// no detached apps on other boards
false
}
};

#[cfg(feature = "bao1x")]
{
csprng.random_delay();
bao1x_hal::hardening::check_pll();
// Follow up the mesh check in loader - it takes 100ms or so for the mesh to settle, we can't afford
// to wait that long in boot1.
let one_way = bao1x_hal::acram::OneWayCounter::new();
bao1x_hal::hardening::mesh_check_and_react(&mut csprng, &one_way);
}

#[cfg(not(feature = "bao1x"))]
let detached_app = false;

// the kernel arg buffer is SIG_BLOCK_SIZE into the signed region
#[cfg(not(feature = "bao1x"))]
let signature_size = SIGBLOCK_SIZE;
#[cfg(feature = "bao1x")]
let signature_size = bao1x_api::signatures::SIGBLOCK_LEN;
let arg_buffer = (signed_buffer as u32 + signature_size as u32) as *const usize;
println!("arg_buffer: {:x}", arg_buffer as usize);

// perhaps later on in these sequences, individual sub-images may be validated
// against sub-signatures; or the images may need to be re-validated after loading
// into RAM, if we have concerns about RAM glitching as an attack surface (I don't think we do...).
// But for now, the basic "validate everything as a blob" is perhaps good enough to
// armor code-at-rest against front-line patching attacks.
let kab = KernelArguments::new(arg_buffer);
boot_sequence(kab, signature, fs_prehash, perclk_freq, detached_app);
}

fn boot_sequence(
args: KernelArguments,
_signature: u32,
fs_prehash: [u8; 64],
_perclk_freq: u32,
detached_app: bool,
) -> ! {
#[cfg(feature = "board-baosec")]
let mut iox = bao1x_hal::iox::Iox::new(utralib::utra::iox::HW_IOX_BASE as *mut u32);
#[cfg(feature = "board-baosec")]
let mut udma_global = bao1x_hal::udma::GlobalConfig::new();
#[cfg(feature = "board-baosec")]
let mut oled = crate::platform::oled_init(&mut iox, &mut udma_global, _perclk_freq);
#[cfg(feature = "board-baosec")]
crate::platform::progress_bar(&mut oled, 0);

// Store the initial boot config on the stack. We don't know
// where in heap this memory will go.
#[allow(clippy::cast_ptr_alignment)] // This test only works on 32-bit systems
let mut cfg = BootConfig { base_addr: args.base as *const usize, args, ..Default::default() };
#[cfg(feature = "swap")]
println!("Size of BootConfig: {:x}", core::mem::size_of::<BootConfig>());
read_initial_config(&mut cfg);

#[cfg(feature = "swap")]
{
cfg.swap_hal = SwapHal::new(&cfg, _perclk_freq);
read_swap_config(&mut cfg);
}
if detached_app {
read_detached_app_config(&mut cfg);
}

// check to see if we are recovering from a clean suspend or not
#[cfg(feature = "resume")]
let (clean, was_forced_suspend, susres_pid) = check_resume(&mut cfg);
#[cfg(not(feature = "resume"))]
let clean = false;
if !clean {
#[cfg(not(feature = "bao1x"))]
{
// setup heap so we can make env. It's not set up earlier in precursor environment
// because it's not safe to smash memory on a resume
let heap_start = utralib::HW_SRAM_EXT_MEM + HEAP_OFFSET;
// for precursor, clear this region, as it is only cleared later in the boot process
let ram_init = utralib::HW_SRAM_EXT_MEM as *mut u32;
for i in 0..(HEAP_LEN + HEAP_OFFSET) / size_of::<u32>() {
unsafe { ram_init.add(i).write_volatile(0) };
}
println!("Setting up heap @ {:x}-{:x}", heap_start, heap_start + HEAP_LEN);
unsafe {
ALLOCATOR.lock().init(heap_start as *mut u8, HEAP_LEN);
}
}
// build the environment variables - requires heap
let mut env_variables = crate::env::EnvVariables::new();
env_variables.add_var("ROOT_FILESYSTEM_HASH", &crate::env::to_hex_ascii(&fs_prehash));

#[cfg(feature = "bao1x")]
{
let owc = bao1x_hal::acram::OneWayCounter::new();
let slot_mgr = bao1x_hal::acram::SlotManager::new();
let sn = bao1x_hal::usb::derive_usb_serial_number(&owc, &slot_mgr);
env_variables.add_var("PUBLIC_SERIAL", &sn);
let hex_uuid = hex::encode(slot_mgr.read(&UUID).unwrap());
env_variables.add_var("UUID", &hex_uuid);
}

// cold boot path
println!("No suspend marker found, doing a cold boot!");
clear_ram(&mut cfg);

#[cfg(feature = "board-baosec")]
{
phase_1(&mut cfg, detached_app, Some(&mut oled));
phase_2(&mut cfg, env_variables, Some(&mut oled));
crate::platform::progress_bar(&mut oled, 100);
}
#[cfg(not(feature = "board-baosec"))]
{
phase_1(&mut cfg, detached_app, None);
phase_2(&mut cfg, env_variables, None);
}

#[cfg(any(feature = "debug-print", feature = "swap"))]
if VDBG || SDBG {
check_load(&mut cfg);
}
println!("done initializing for cold boot.");
#[cfg(feature = "bao1x")]
{
// cleanup the leaked key to the backup registers
let mut buram = bao1x_hal::buram::BackupManager::new();
bao1x_api::bollard!(bao1x_hal::sigcheck::die_no_std, 4);
// safety: this specific region is not part of the hash-checked register bank
unsafe {
buram.store_slice_no_hash(&[0u8; 32], bao1x_hal::buram::ERASURE_PROOF_RANGE_BYTES.start);
}
bao1x_api::bollard!(bao1x_hal::sigcheck::die_no_std, 4);
}
}
#[cfg(feature = "resume")]
if clean {
// resume path
use utralib::generated::*;
// flip my self-power-on switch: otherwise, I might turn off before the whole sequence is finished.
let mut power_csr = CSR::new(utra::power::HW_POWER_BASE as *mut u32);
power_csr.rmwf(utra::power::POWER_STATE, 1);
power_csr.rmwf(utra::power::POWER_SELF, 1);

// TRNG virtual memory mapping already set up, but we pump values out just to make sure
// the pipeline is fresh. Simulations show this isn't necessary, but I feel paranoid;
// I worry a subtle bug in the reset logic could leave deterministic values in the pipeline.
let trng_csr = CSR::new(utra::trng_kernel::HW_TRNG_KERNEL_BASE as *mut u32);
for _ in 0..4 {
while trng_csr.rf(utra::trng_kernel::URANDOM_VALID_URANDOM_VALID) == 0 {}
trng_csr.rf(utra::trng_kernel::URANDOM_URANDOM);
}
// turn on the kernel UART.
let mut uart_csr = CSR::new(utra::uart::HW_UART_BASE as *mut u32);
uart_csr.rmwf(utra::uart::EV_ENABLE_RX, 1);

// setup the \\`susres\\` register for a resume
let mut resume_csr = CSR::new(utra::susres::HW_SUSRES_BASE as *mut u32);
// set the resume marker for the SUSRES server, noting the forced suspend status
if was_forced_suspend {
resume_csr.wo(
utra::susres::STATE,
resume_csr.ms(utra::susres::STATE_RESUME, 1)
| resume_csr.ms(utra::susres::STATE_WAS_FORCED, 1),
);
} else {
resume_csr.wfo(utra::susres::STATE_RESUME, 1);
}
resume_csr.wfo(utra::susres::CONTROL_PAUSE, 1); // ensure that the ticktimer is paused before resuming
resume_csr.wfo(utra::susres::EV_ENABLE_SOFT_INT, 1); // ensure that the soft interrupt is enabled for the kernel to kick
println!("clean suspend marker found, doing a resume!");

// trigger the interrupt; it's not immediately handled, but rather checked later on by the kernel on
// clean resume
resume_csr.wfo(utra::susres::INTERRUPT_INTERRUPT, 1);
}

// condense debug and resume arguments into a single register, so we have space for XPT
let debug_resume = if cfg.debug { 0x1 } else { 0x0 } | if clean { 0x2 } else { 0x0 };
if !clean {
#[cfg(feature = "bao1x")]
{
// clean it twice, just in case the first cleanup was glitched past
let mut buram = bao1x_hal::buram::BackupManager::new();
bao1x_api::bollard!(bao1x_hal::sigcheck::die_no_std, 4);
// safety: this specific region is not part of the hash-checked register bank
unsafe {
buram.store_slice_no_hash(&[0u8; 32], bao1x_hal::buram::ERASURE_PROOF_RANGE_BYTES.start);
}
bao1x_api::bollard!(bao1x_hal::sigcheck::die_no_std, 4);
}

// The MMU should be set up now, and memory pages assigned to their
// respective processes.
let krn_struct_start = cfg.sram_start as usize + cfg.sram_size - cfg.init_size + cfg.swap_offset;
#[cfg(feature = "swap")]
if SDBG && VDBG {
const CHUNK_SIZE: usize = 2;
// activate to debug stack smashes. RPT should be 0's here (or at least valid PIDs) if stack did
// not overflow.
for (_i, _r) in cfg.runtime_page_tracker[cfg.runtime_page_tracker.len() - 512..]
.chunks(CHUNK_SIZE)
.enumerate()
{
println!(" rpt {:08x}: {:02x?}", cfg.runtime_page_tracker.len() - 512 + _i * CHUNK_SIZE, _r);
}
}
// Add a static check for stack overflow, using a heuristic that the last 64 entries of the RPT
// ought to be a valid PID. A stack smash is likely to write something that does not obey
// this heuristic within that range (any stack-stored pointer, for example, will break this).
for &check in cfg.runtime_page_tracker[cfg.runtime_page_tracker.len() - 64..].iter() {
assert!(
// use .to_le() to access the structure because SwapAlloc can either be a u8 or a composite
// type, and .to_le() can do the right thing for both cases.
check.to_le() <= cfg.processes.len() as u8,
"RPT looks corrupted, suspect stack overflow in loader. Increase GUARD_MEMORY_BYTES!"
);
}
// compute the virtual addresses of all of these "manually"
let arg_offset = cfg.args.base as usize - krn_struct_start + KERNEL_ARGUMENT_OFFSET;
let ip_offset = cfg.processes.as_ptr() as usize - krn_struct_start + KERNEL_ARGUMENT_OFFSET;
let rpt_offset =
cfg.runtime_page_tracker.as_ptr() as usize - krn_struct_start + KERNEL_ARGUMENT_OFFSET;
let xpt_offset = cfg.extra_page_tracker.as_ptr() as usize - krn_struct_start + KERNEL_ARGUMENT_OFFSET;
#[cfg(not(feature = "atsama5d27"))]
let _tt_addr = { cfg.processes[0].satp };
#[cfg(feature = "atsama5d27")]
let _tt_addr = { cfg.processes[0].ttbr0 };
println!(
"Jumping to kernel @ {:08x} with map @ {:08x} and stack @ {:08x} (kargs: {:08x}, ip: {:08x}, rpt: {:08x}, xpt: {:08x})",
cfg.processes[0].entrypoint,
_tt_addr,
cfg.processes[0].sp,
arg_offset,
ip_offset,
rpt_offset,
xpt_offset,
);

// save a copy of the computed kernel registers at the bottom of the page reserved
// for the bootloader stack. Note there is no stack-smash protection for these arguments,
// so we're definitely vulnerable to e.g. buffer overrun attacks in the early bootloader.
//
// Probably the right long-term solution to this is to turn all the bootloader "loading"
// actions into "verify" actions during a clean resume (right now we just don't run them),
// so that attempts to mess with the args during a resume can't lead to overwriting
// critical parameters like these kernel arguments.
#[cfg(not(feature = "atsama5d27"))]
unsafe {
let backup_args: *mut [u32; 8] = BACKUP_ARGS_ADDR as *mut [u32; 8];
(*backup_args)[0] = arg_offset as u32;
(*backup_args)[1] = ip_offset as u32;
(*backup_args)[2] = rpt_offset as u32;
(*backup_args)[3] = cfg.processes[0].satp as u32;
(*backup_args)[4] = cfg.processes[0].entrypoint as u32;
(*backup_args)[5] = cfg.processes[0].sp as u32;
(*backup_args)[6] = if cfg.debug { 1 } else { 0 };
(*backup_args)[7] = xpt_offset as u32;
#[cfg(feature = "debug-print")]
{
if VDBG {
println!("Backup kernel args:");
for &arg in (*backup_args).iter() {
println!("0x{:08x}", arg);
}
}
}
}

#[cfg(not(feature = "atsama5d27"))]
{
#[cfg(not(any(feature = "bao1x")))]
{
// uart mux only exists on the FPGA variant
use utralib::generated::*;
let mut gpio_csr = CSR::new(utra::gpio::HW_GPIO_BASE as *mut u32);
#[cfg(not(feature = "early-printk"))]
gpio_csr.wfo(utra::gpio::UARTSEL_UARTSEL, 1); // patch us over to a different UART for debug (1=LOG 2=APP, 0=KERNEL(hw reset default))
#[cfg(feature = "early-printk")]
gpio_csr.wfo(utra::gpio::UARTSEL_UARTSEL, 0); // on early printk, leave us on the kernel owning the UART
}

start_kernel(
arg_offset,
ip_offset,
rpt_offset,
xpt_offset,
cfg.processes[0].satp,
cfg.processes[0].entrypoint,
cfg.processes[0].sp,
debug_resume,
);
}

#[cfg(feature = "atsama5d27")]
unsafe {
start_kernel(
cfg.processes[0].sp,
cfg.processes[0].ttbr0,
cfg.processes[0].entrypoint,
arg_offset,
ip_offset,
rpt_offset,
xpt_offset,
debug_resume,
)
}
} else {
#[cfg(feature = "resume")]
unsafe {
let backup_args: *mut [u32; 8] = BACKUP_ARGS_ADDR as *mut [u32; 8];
#[cfg(feature = "debug-print")]
{
println!("Using backed up kernel args:");
for &arg in (*backup_args).iter() {
println!("0x{:08x}", arg);
}
}
let satp = ((*backup_args)[3] as usize) & 0x803F_FFFF | (((susres_pid as usize) & 0x1FF) << 22);
//let satp = (*backup_args)[3];
println!(
"Adjusting SATP to the sures process. Was: 0x{:08x} now: 0x{:08x}",
(*backup_args)[3],
satp
);

#[cfg(not(feature = "renode-bypass"))]
{
use utralib::generated::*;
let mut gpio_csr = CSR::new(utra::gpio::HW_GPIO_BASE as *mut u32);
gpio_csr.wfo(utra::gpio::UARTSEL_UARTSEL, 0); // patch us over to a different UART for debug (1=LOG 2=APP, 0=KERNEL(default))
}

start_kernel(
(*backup_args)[0] as usize,
(*backup_args)[1] as usize,
(*backup_args)[2] as usize,
(*backup_args)[7] as usize,
satp as usize,
(*backup_args)[4] as usize,
(*backup_args)[5] as usize,
debug_resume,
);
}
#[cfg(not(feature = "resume"))]
panic!("Unreachable code executed");
}
}

pub fn read_initial_config(cfg: &mut BootConfig) {
let args = cfg.args;
let mut i = args.iter();
let xarg = i.next().expect("couldn't read initial tag");
if xarg.name != u32::from_le_bytes(*b"XArg") || xarg.size != 20 {
panic!("XArg wasn't first tag, or was invalid size");
}
cfg.sram_start = xarg.data[2] as *mut usize;
cfg.sram_size = xarg.data[3] as usize;
#[cfg(feature = "swap")]
if SDBG {
println!("XAarg // sram_start: {:x}, sram_size: {:x}", cfg.sram_start as usize, cfg.sram_size);
}

let mut kernel_seen = false;
let mut init_seen = false;

for tag in i {
if tag.name == u32::from_le_bytes(*b"MREx") {
cfg.regions = unsafe {
slice::from_raw_parts(
tag.data.as_ptr() as *const MemoryRegionExtra,
tag.size as usize / mem::size_of::<MemoryRegionExtra>(),
)
};
} else if tag.name == u32::from_le_bytes(*b"Bflg") {
let boot_flags = tag.data[0];
if boot_flags & 1 != 0 {
cfg.no_copy = true;
}
if boot_flags & (1 << 1) != 0 {
cfg.base_addr = core::ptr::null::<usize>();
}
if boot_flags & (1 << 2) != 0 {
cfg.debug = true;
}
} else if tag.name == u32::from_le_bytes(*b"XKrn") {
assert!(!kernel_seen, "kernel appears twice");
assert!(tag.size as usize == mem::size_of::<ProgramDescription>(), "invalid XKrn size");
kernel_seen = true;
} else if tag.name == u32::from_le_bytes(*b"IniE") || tag.name == u32::from_le_bytes(*b"IniF") {
assert!(tag.size >= 4, "invalid Init size");
init_seen = true;
cfg.init_process_count += 1;
}
#[cfg(feature = "swap")]
if tag.name == u32::from_le_bytes(*b"Swap") {
// safety: the image creator guarantees this data is aligned and initialized properly
cfg.swap = Some(unsafe { &*(tag.data.as_ptr() as *const crate::swap::SwapDescriptor) });
}
}

assert!(kernel_seen, "no kernel definition");
assert!(init_seen, "no initial programs found");
}

#[cfg(feature = "swap")]
pub fn read_swap_config(cfg: &mut BootConfig) {
// Read in the swap arguments: should be located at beginning of the encrypted image in swap.
let page0 = cfg.swap_hal.as_mut().unwrap().decrypt_src_page_at(0x0).unwrap();
let swap_args = KernelArguments::new(page0.as_ptr() as *const usize);
for tag in swap_args.iter() {
if tag.name == u32::from_le_bytes(*b"IniS") {
assert!(tag.size >= 4, "invalid Init size");
cfg.init_process_count += 1;
} else if tag.name == u32::from_le_bytes(*b"XArg") {
// these are actually specified inside the \\`Swap\\` arg, this is just
// a mirror because this argument is added by default by the image creator
if SDBG {
println!("Swap start: {:x}", tag.data[2]);
println!("Swap size: {:x}", tag.data[3]);
}
} else {
println!("Unhandled argument in swap: {:x}", tag.name);
}
}
}

pub fn read_detached_app_config(cfg: &mut BootConfig) {
let app_args = KernelArguments::new(
(bao1x_api::offsets::dabao::APP_RRAM_START + bao1x_api::signatures::SIGBLOCK_LEN) as *const usize,
);
for tag in app_args.iter() {
if tag.name == u32::from_le_bytes(*b"IniF") {
assert!(tag.size >= 4, "invalid Init size");
cfg.init_process_count += 1;
}
}
}

/// Checks a reserved area of RAM for a pattern with a pre-defined mathematical
/// relationship. The purpose is to detect if we have a "clean suspend", or if
/// we've rebooted from a corrupt/cold RAM state.
#[cfg(feature = "resume")]
fn check_resume(cfg: &mut BootConfig) -> (bool, bool, u32) {
use utralib::generated::*;
const WORDS_PER_SECTOR: usize = 128;
const NUM_SECTORS: usize = 8;
const WORDS_PER_PAGE: usize = PAGE_SIZE / 4;

let suspend_marker = cfg.sram_start as usize + cfg.sram_size - GUARD_MEMORY_BYTES;
let marker: *mut [u32; WORDS_PER_PAGE] = suspend_marker as *mut [u32; WORDS_PER_PAGE];

let boot_seed = CSR::new(utra::seed::HW_SEED_BASE as *mut u32);
let seed0 = boot_seed.r(utra::seed::SEED0);
let seed1 = boot_seed.r(utra::seed::SEED1);
let was_forced_suspend: bool = if unsafe { (*marker)[0] } != 0 { true } else { false };

let mut clean = true;
let mut hashbuf: [u32; WORDS_PER_SECTOR - 1] = [0; WORDS_PER_SECTOR - 1];
let mut index: usize = 0;
let mut pid: u32 = 0;
for sector in 0..NUM_SECTORS {
for i in 0..hashbuf.len() {
hashbuf[i] = unsafe { (*marker)[index * WORDS_PER_SECTOR + i] };
}
// sector 0 contains the boot seeds, which we replace with our own as read out from our FPGA before
// computing the hash it also contains the PID of the suspend/resume process manager, which we
// need to inject into the SATP
if sector == 0 {
hashbuf[1] = seed0;
hashbuf[2] = seed1;
pid = hashbuf[3];
}
let hash = crate::murmur3::murmur3_32(&hashbuf, 0);
if hash != unsafe { (*marker)[(index + 1) * WORDS_PER_SECTOR - 1] } {
println!("* computed 0x{:08x} - stored 0x{:08x}", hash, unsafe {
(*marker)[(index + 1) * (WORDS_PER_SECTOR) - 1]
});
clean = false;
} else {
println!(" computed 0x{:08x} - match", hash);
}
index += 1;
}
// zero out the clean suspend marker, so if something goes wrong during resume we don't try to resume
// again
for i in 0..WORDS_PER_PAGE {
unsafe {
(*marker)[i] = 0;
}
}

(clean, was_forced_suspend, pid)
}

/// Clears all of RAM. This is a must for systems that have suspend-to-RAM for security.
/// It is configured to be skipped in simulation only, to accelerate the simulation times
/// since we can initialize the RAM to zero in simulation.
#[cfg(all(not(feature = "simulation-only")))]
fn clear_ram(cfg: &mut BootConfig) {
// clear RAM on a cold boot.
// RAM is persistent and battery-backed. This means secret material could potentially
// stay there forever, if not explicitly cleared. This clear adds a couple seconds
// to a cold boot, but it's probably worth it. Note that it doesn't happen on a suspend/resume.
let ram: *mut u32 = cfg.sram_start as *mut u32;
#[cfg(feature = "swap")]
let clear_limit = GUARD_MEMORY_BYTES;
#[cfg(not(feature = "swap"))]
let clear_limit = ((4096 + core::mem::size_of::<BootConfig>()) + 4095) & !4095;
if VDBG {
println!("Stack clearing limit: {:x}", clear_limit);
}
let clear_start = HEAP_OFFSET + HEAP_LEN;
unsafe {
for addr in clear_start..(cfg.sram_size - clear_limit) / 4 {
// 8k is reserved for our own stack
ram.add(addr).write_volatile(0);
}
}
}

pub unsafe fn bzero<T>(mut sbss: *mut T, ebss: *mut T) {
if VDBG {
println!("ZERO: {:08x} - {:08x}", sbss as usize, ebss as usize);
}
while sbss < ebss {
// NOTE(volatile) to prevent this from being transformed into \\`memclr\\`
// which can create an accidental dependency on libc.
ptr::write_volatile(sbss, mem::zeroed());
sbss = sbss.offset(1);
}
}

/// This function allows us to check the final loader results
/// It will print to the console the first 32 bytes of each loaded
/// region top/bottom, based upon extractions from the page table.
#[cfg(any(feature = "debug-print", feature = "swap"))]
fn check_load(cfg: &mut BootConfig) {
let args = cfg.args;

// This is the offset in RAM where programs are loaded from.
println!("\\n\\nCHECKING LOAD");

// Go through all Init processes and the kernel, setting up their
// page tables and mapping memory to them.
let mut pid = 2;
for tag in args.iter() {
if tag.name == u32::from_le_bytes(*b"IniE") {
let inie = MiniElf::new(&tag);
println!("\\n\\nChecking IniE region");
inie.check(cfg, inie.load_offset as usize, pid, IniType::IniE);
pid += 1;
} else if tag.name == u32::from_le_bytes(*b"IniF") {
let inif = MiniElf::new(&tag);
println!("\\n\\nChecking IniF region");
inif.check(cfg, inif.load_offset as usize, pid, IniType::IniF);
pid += 1;
} else if tag.name == u32::from_le_bytes(*b"IniS") {
let inis = MiniElf::new(&tag);
println!("\\n\\nChecking IniS region");
inis.check(cfg, inis.load_offset as usize, pid, IniType::IniS);
pid += 1;
}
}
}

// Install a panic handler when not running tests.
#[cfg(all(not(test), not(feature = "atsama5d27")))]
mod panic_handler {
use core::panic::PanicInfo;
#[panic_handler]
fn handle_panic(_arg: &PanicInfo) -> ! {
crate::println!("{}", _arg);
loop {
#[cfg(feature = "bao1x")]
bao1x_hal::sigcheck::die_no_std();
}
}
}

Served by rngit 1.4.0 - Generated in 0.06s